iT邦幫忙

2023 iThome 鐵人賽

DAY 3
0

今天我們繼續分享tips 6~10

6. 當function回傳值為bool

有時候,我們會寫一些輔助的function來判斷傳入值是否符合某些條件,符合回傳True,不符合回傳False

  • 一般我們會寫出像# 06a的模式,利用if-else來實作。
# 06a
def get_bool(iterable):
    if len(iterable) > 10:
        return True
    else:
        return False


if __name__ == '__main__':
    print(get_bool(range(10)), get_bool(range(11)))    # False, True
  • 但仔細看看,else的部份,其實可以省略,所以也可以寫成# 06b的模式。
# 06b
def get_bool(iterable):
    if len(iterable) > 10:
        return True
    return False


if __name__ == '__main__':
    print(get_bool(range(10)), get_bool(range(11)))    # False, True
  • len(iterable) > 10本身就會回傳True或是False,可以直接返回。所以就有了# 06c的模式。我們覺得# 06c這種模式俗又有力,推薦您使用。
# 06c
def get_bool(iterable):
    return len(iterable) > 10


if __name__ == '__main__':
    print(get_bool(range(10)), get_bool(range(11)))    # False, True

7. print to file

當我們有一個內含多個strlist要寫入檔案,且每個str需要自己獨立一行。

  • 一般會寫出# 07a中的模式,手動於每次寫入時,加入\n
# 07a
text = ['abcde', '12345']

with open('file1.txt', 'w') as f:
    for s in text:
        f.write(s+'\n')
  • 由於print接受一個file參數,預設是sys.stdout,我們可以偷龍轉鳳將open之後的f指定給file,讓print來幫我們自動加入\n,如# 07b所示。
# 07b
text = ['abcde', '12345']

with open('file2.txt', 'w') as f:
    for s in text:
        print(s, file=f)
  • 又由於print可以接受多個參數,所以我們可以進一步改寫# 07b# 07c,直接傳入*text,並指定sep='\n'來節省一個迴圈。# 07c的寫法善用built-in function,且相當pythonic,推薦給您。
# 07c
text = ['abcde', '12345']

with open('file3.txt', 'w') as f:
    print(*text, sep='\n', file=f)

最後我們做個檢查。# 07d可以使用()將context manager分成多行的語法,為Python 3.10新添增的。一般而言,context manager的命名會偏長,有了這個語法幫忙後,code看起來會清楚不少。

# 07d
with (open('file1.txt') as f1,
      open('file2.txt') as f2,
      open('file3.txt') as f3):
    print(f1.read() == f2.read() == f3.read())  # True

8. list unpacking

當有一個list包含一個tuple時,如[('d', 4)]的情況,該如何取得'd'4呢?

一般的解法是先利用[('d', 4)][0]來拿到內層的('d', 4),然後使用d, four = ('d', 4)tuple unpacking,如# 08a

# 08a
from collections import Counter

iterable = 'a'*1 + 'b'*2 + 'c'*3 + 'd'*4
cnter = Counter(iterable)
# cnter.most_common(1)  # [('d', 4)]

if __name__ == '__main__':
    # tuple unpacking
    d, four = cnter.most_common(1)[0]
    print(f'{d=}, {four=}')  # d='d', four=4

針對這種情況,我們會推薦使用list unpacking [(d, four)] = [('d', 4)],如# 08b。沒錯,[]也能放在等號的左側。於此處使用list unpacking的好處是可以免去最外層需使用listindexing,且擁有類似像Rust的語法,可以驗證左右側整體型式是否相同(list包含一個tuple,且tuple內元素數量要左右相同)。

# 08b
from collections import Counter

iterable = 'a'*1 + 'b'*2 + 'c'*3 + 'd'*4
cnter = Counter(iterable)
# cnter.most_common(1)  # [('d', 4)]

if __name__ == '__main__':
    # list unpacking
    [(d, four)] = cnter.most_common(1)
    print(f'{d=}, {four=}')  # d='d', four=4

此外list unpacking也支援像tuple unpacking一樣的*語法,如# 08c,來收集剩餘的元素。

# 08c
from collections import Counter

iterable = 'a'*1 + 'b'*2 + 'c'*3 + 'd'*4
cnter = Counter(iterable)
# cnter.most_common(1)  # [('d', 4)]

if __name__ == '__main__':
    # `*` can be used in list unpacking as well
    [(a, *_)] = [('a', 'b', 'c')]
    print(f'{a=}, {_=}')  # a='a', _=['b', 'c']

list unpacking雖然少見,但在這種特別的情形,我們會推薦使用。畢竟因為比較少用的關係,當自己在讀code看到時,很容易會注意到,反而會格外留心檢查右側的整體型別,或許這也是件好事吧XD

9. zip與dict的搭配使用

一般大家會使用zip的情況,應該是需要對多個iterable打迴圈的時候才會用到,但我們發現zip搭配dict也有許多妙用。

當需要一個dict,其keysa~z,而values1~26,您會如何建立呢(註1)?
一般來說,您應該會利用dict-comprehension,如# 09a

# 09a
from string import ascii_lowercase

if __name__ == '__main__':
    # [str, int]
    d = {k: v
         for k, v in zip(ascii_lowercase, range(1, 27))}

但我們發覺# 09b這種寫法更加優雅。zip幫我們將ascii_lowercasecount(1)縫在一起,然後交給dict幫忙生成。短短一行,我們靈活地使用了zipitertools.count

zip的強大,不只在於縫製iterable,也可以體現於拆iterable。當您有一個dict,您會如何同時取得keysvalues呢?

  • 一般來說,您應該會使用dict.keys()dict.values()
  • keys, values = zip(*d.items())是一種拆掉d的方法。這個方法得到的keysvaluestuple型態,所以可以使用[]來取值。而dict.keys()得到的dict_keys型態與dict.values()得到的dict_values型態,皆無法使用[]
# 09b
from itertools import count
from string import ascii_lowercase

if __name__ == '__main__':
    # [str, int]
    d = dict(zip(ascii_lowercase, count(1)))
    keys, values = zip(*d.items())

10. 利用continue減少縮排

continue是一個於迴圈中離開當下這個cycle,進入下一個cycle的語法。

假設我們想要寫一個my_filter function註2),其要求如下:

  • 接受兩個參數funciterablefunc會針對iterable中每個element給出TrueFalse
  • 最後my_filter會以generator型式返回func判斷為True的元素。

一般來說,我們會寫出如# 10a的模式。但此時我們由於forif,程式的主邏輯yield item得出現於第三層。如果外面層數過多,例如有使用context manager、多個迴圈或多個if,整體程式碼會很難閱讀。

# 10a
def func(x):
    try:
        return x > 10
    except TypeError:
        return False


def my_filter(func, iterable):
    """`yield item` locates at the second level of indentation"""
    for item in iterable:
        if func(item):
            yield item


if __name__ == '__main__':
    flt = my_filter(func, [2, 11, 'str', (), []])
    print(list(flt))  # [11]

如果我們搭配notcontinue,則會寫出# 10b的模式。這麼一來我們就只縮排了for這一層,程式的主邏輯yield item可以出現於第二層。

# 10b
def func(x):
    try:
        return x > 10
    except TypeError:
        return False


def my_filter(func, iterable):
    """`yield item` locates at the first level of indentation
        by using `continue` to save one level of indentation"""
    for item in iterable:
        if not func(item):
            continue
        yield item


if __name__ == '__main__':
    flt = my_filter(func, [2, 11, 'str', (), []])
    print(list(flt))  # [11]

# 10a的方式較為直觀,但如若程式碼已縮排多層又很難重構的話,可以嘗試採用# 10b的寫法。

備註

註1:如果想要反過來,建立一個keys1~26valuesa-zdict,可以這麼做:

# 09c
from string import ascii_lowercase

if __name__ == '__main__':
    # [int, str]
    d = dict(enumerate(ascii_lowercase, 1))

註2:Python的filter實作大致如下,我們實作的是個閹割的版本。

# 10c
def my_filter(func, iterable):
    """Emulate built-in filter"""
    if func is not None:
        return (item for item in iterable if func(item))
    return (item for item in iterable if item)

Code

本日程式碼傳送門


上一篇
[Day02] 初翼 - Tips:1~5
下一篇
[Day04] 初翼 - Tips:11~15
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言